﻿using System;
using System.Linq;
using System.IO;
using System.Drawing;
using System.Collections;

namespace NumberRecognition.Models
{
    [Serializable]
    public class NeuralNet
    {
        //private const int PictureSize = 28 * 28;//180 * 200;
        private const int PerceptronsInOutputLayer = 10;
        //private const int FirstHiddenLayerInputs = 1; //кол-во сигналов на вход одного нейрона в скрытом слое        
        private const int HiddenLayers = 0;

        private readonly float _learningSpeed;
        private readonly Neuron[][] _layers;

        public float E { get; set; } //энергия ошибки(сумма квадратов ошибок)
        //public float Eav { get; set; } //средняя энергия ошибки(мера эффективности обучения)

        public NeuralNet(float ls, int pictureSize)
        {
            /*т.к. задача входного слоя передать входные значения дальше, то он используется только как абстракция, 
             * т.е. на самом деле его нет, а входные значения передаем сразу нейронам в скрытом слое */

            _learningSpeed = ls;

            _layers = new Neuron[HiddenLayers + 1][]; //последний слой выходной, остальные скрытые                

            for (var i = 0; i < _layers.Length; i++) //для скрытых слоёв
            {
                if (i < (_layers.Length - 1)) //если не последний слой
                    /* в каждом след скрытом слое будет в 2 раза меньше нейронов, чем в предыдущем */
                    _layers[i] = new Neuron[pictureSize / (i == 0 ? 2 : (2 * i))];
                else
                    _layers[i] = new Neuron[PerceptronsInOutputLayer]; //для выходного слоя

                for (int j = 0; j < _layers[i].Length; j++)
                {
                    if (i == 0) //если первый скрытый слой, играющий роль входного
                        _layers[i][j] = new Neuron(pictureSize);
                    else
                        _layers[i][j] = new Neuron(_layers[i - 1].Length);

                    _layers[i][j].SetRandomWeights();
                }
            }
        }

        private float[] ForwardPass(float[] x)
        {
            float[] y = null;

            //перебираем слои
            foreach (var layer in _layers)
            {
                y = new float[layer.Length]; //массив выходных сигналов для каждого нейрона

                //идем по нейронам
                for (var j = 0; j < layer.Length; j++)
                {
                    //для каждого нейрона получаем сигнал
                    y[j] = layer[j].ActivationFunction(x);
                }

                x = y; //выходной сигнал предыдущего слоя, становиться входным для следующего
            }

            return y;
        }

        private void BackwardPass(float[] x, float[] y, int d)
        {
            float[] e = new float[y.Length]; //сигнал ошибки на выходе нейрона выходного слоя
            float[] localGradient, localGradientPrevious;
            int indexOfPreviousLayer = -1;
            E = 0.0F;

            for (int i = 0; i < y.Length; i++) //вычисляем сигнал ошибки для каждого нейрона вых.слоя
            {
                if (i == d)
                    e[i] = 1.0F - y[i];
                else
                    e[i] = 0.0F - y[i];

                E += (0.5F) * (float)(Math.Pow(e[i], 2));
            }

            localGradientPrevious = new float[0]; //инициализируем какой-нить херней, чтобы компилятор не ругался

            //идем по слоям в обратном порядке
            for (var i = _layers.Length - 1; i >= 0; i--)
            {
                localGradient = new float[_layers[i].Length];

                //идем по нейронам
                for (var j = 0; j < _layers[i].Length; j++)
                {
                    if (i == (_layers.Length - 1))//если выходной слой                                               
                        localGradient[j] = e[j] * _layers[i][j].DerivativeFunction();
                    else
                    {
                        float sum = 0;
                        for (int k = 0; k < _layers[indexOfPreviousLayer].Length; k++)
                            sum += localGradientPrevious[k] * _layers[indexOfPreviousLayer][k].GetWeight(j + 1); //+1 т.к. первый порог                         

                        localGradient[j] = _layers[i][j].DerivativeFunction() * sum;
                    }

                    if (i == 0)
                    {
                        //_layers[i][j].CorrectWeight(_learningSpeed, localGradient[j], 1.0F, x.Length);

                        for (int k = 0; k < x.Length; k++)
                            _layers[i][j].CorrectWeight(_learningSpeed, localGradient[j], x[k], k + 1); //Первый порог
                    }
                    else
                    {
                        //_layers[i][j].CorrectWeight(_learningSpeed, localGradient[j], 1.0F, _layers[i - 1].Length);

                        for (int k = 0; k < _layers[i - 1].Length; k++)
                            _layers[i][j].CorrectWeight(_learningSpeed, localGradient[j], _layers[i - 1][k].GetOutput(), k + 1);
                    }
                }

                indexOfPreviousLayer = i;
                localGradientPrevious = new float[_layers[i].Length];
                localGradientPrevious = localGradient;
            }
        }

        public void Learn(float[] x, int d)
        {
            float[] y;
            float output;
            int digit = -1;

            //while (true)
            //{
            y = ForwardPass(x);

            output = y.Max(); //Будем считатать, что правильный ответ тот, где максимальный выход

            for (int i = 0; i < y.Length; i++)
            {
                if (y[i] == output)
                {
                    digit = i;
                    break;
                }
            }

            if (d == digit)
                //break;
                return;

            BackwardPass(x, y, d);
            //}
        }

        public int TryToRecognize(float[] x)
        {
            //RecognizeTrainingSet();

            float[] y = ForwardPass(x);
            float output = y.Max();

            for (int i = 0; i < y.Length; i++)
            {
                if (y[i] == output)
                    return i;
            }

            return -1;
        }
        //УДАЛИТЬ ПОТОМ 
        public void RecognizeTrainingSet()
        {
            var PathToLearningSet = "../../numbers/"; /////////// ПОТОМ УБРАТЬ!!!!!!!!!!!!!            
            ArrayList resultNotGood = new ArrayList();
            ArrayList resultGood = new ArrayList();
            Bitmap image;

            foreach (var imgName in Directory.GetFiles(PathToLearningSet, "*.jpeg"))
            {
                image = new Bitmap(imgName);

                var x = GetPixels(image);

                float[] y = ForwardPass(x);
                float output = y.Max();

                for (int i = 0; i < y.Length; i++)
                {
                    if (y[i] == output)
                        output = i;
                }

                int d = Convert.ToInt32(imgName.Substring(imgName.LastIndexOf('_') + 1, 1)); // желаемый отклик
                string img = imgName.Substring(imgName.LastIndexOf('\\') + 1);

                if (output != d)
                    resultNotGood.Add(string.Format("Digit {0}, imgName {1} : {2}", d, img, output));
                else
                    resultGood.Add(string.Format("Digit {0}, imgName {1} : {2}", d, img, output));
            }
        }
        //УДАЛИТЬ ПОТОМ 
        private float[] GetPixels(Bitmap image)
        {
            var x = new float[image.Width * image.Height];
            int k = 0;

            for (int i = 0; i < image.Width; i++)
            {
                for (int j = 0; j < image.Height; j++)
                {
                    x[k++] = image.GetPixel(i, j).ToArgb() == Color.White.ToArgb() ? 0 : 1;
                }
            }

            return x;
        }
    }
}
